package sk.stuba.fiit.ltasys.web.mvc.model.service.core;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.swing.tree.TreeNode;
import javax.xml.bind.JAXBElement;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.collections.list.TreeList;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampResponse;
import org.etsi.uri._01903.v1_3.CertificateValuesType;
import org.etsi.uri._01903.v1_3.EncapsulatedPKIDataType;
import org.etsi.uri._01903.v1_3.QualifyingPropertiesType;
import org.etsi.uri._01903.v1_3.XAdESTimeStampType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Service;
import org.w3._2000._09.xmldsig.ObjectType;
import org.w3._2000._09.xmldsig.SignatureType;
import org.w3._2000._09.xmldsig.X509DataType;

import sk.stuba.fiit.ltasys.web.mvc.model.entity.ArchivePackageEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.BinaryDataEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.CertificateEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.CrlEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.EvidenceRecordEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.TimestampEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.UserDataEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.UserEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.service.CertificateService;
import sk.stuba.fiit.ltasys.web.mvc.model.service.CommunicatorService;
import sk.stuba.fiit.ltasys.web.mvc.model.service.CrlService;
import sk.stuba.fiit.ltasys.web.mvc.model.service.UserService;
import sk.stuba.fiit.ltasys.ws.schema.beans.ArchivePackageType;
import sk.stuba.fiit.ltasys.ws.schema.beans.ArchiveResponse;
import sk.stuba.fiit.ltasys.ws.schema.beans.DataRefType;
import sk.stuba.fiit.ltasys.ws.schema.beans.DataType;
import sk.stuba.fiit.ltasys.ws.schema.beans.EvidenceRecordSequenceType;
import sk.stuba.fiit.ltasys.ws.schema.beans.EvidenceRecordType;
import sk.stuba.fiit.ltasys.ws.schema.beans.SignatureRequest;
import sk.stuba.fiit.ltasys.ws.schema.beans.UriRefType;
import sk.stuba.fiit.ltasys.ws.schema.beans.UserType;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

@Service("archiveCreator")
public class ArchiveCreator {

	@Autowired
	private UserService userService;

	@Autowired
	private CrlService crlService;

	@Autowired
	private CertificateService certificateService;

	@Autowired
	private Jaxb2Marshaller marshallerService;

	@Autowired
	private CommunicatorService communicatorService;

	public ArchivePackageEntity makeArchive(SignatureRequest signatureRequest,
			List<String> userFiles, UserType userType) throws Exception {

		ArchivePackageEntity archiv = new ArchivePackageEntity();

		archiv.setDescription(generateArchiveDescription(userFiles));
		archiv.setName("" + System.currentTimeMillis());
		archiv.setArchiveRecord(processArchiveRecord(signatureRequest));
		archiv.setUser(loadUser(userType));
		archiv.setUserData(prepareUserData(signatureRequest, userFiles));
		archiv.setToken(generateUserToken(archiv.getUserData()));
		archiv.getArchiveRecord().iterator().next().setTimestamp(makeProtection(archiv,null));
		archiv.getArchiveRecord().iterator().next().getTimestamp().setLeaf(true);
		return archiv;
	}
	
	
	public ArchivePackageEntity prolongProtection(ArchivePackageEntity archivPackage) throws Exception{
		EvidenceRecordEntity ere = new EvidenceRecordEntity();
		
		List<EvidenceRecordEntity> list = new ArrayList<EvidenceRecordEntity>(archivPackage.getArchiveRecord());
		Collections.sort(list);
		EvidenceRecordEntity ereOld = list.get(list.size()-1);
		
		archivPackage.getArchiveRecord().add(ere);
		ere.setCertificates(processCertificatesForTimeStampResponse(ereOld.getTimestamp()));
		Set<CrlEntity> crls = new HashSet<CrlEntity>();
		crls.add(processCRLForTimeStampResponse(ereOld.getTimestamp()));
		ere.setCrls(crls);
		ere.setCreated(new Date());		
		
		ere.setTimestamp(makeProtection(archivPackage, ere));
		return archivPackage;
	}
	
	
	
	private Set<CertificateEntity> processCertificatesForTimeStampResponse(TimestampEntity tse) throws Exception {
		TimeStampResponse tsr = new TimeStampResponse(tse.getTimestamp());
		BigInteger sn = tsr.getTimeStampToken().getTimeStampInfo().getSerialNumber();
		HashSet<BigInteger> certSn = new HashSet<BigInteger>();
		certSn.add(sn);
		return certificateService.handleCertificate(certSn);
	}


	private CrlEntity processCRLForTimeStampResponse (TimestampEntity tse)throws Exception {
		TimeStampResponse tsr = new TimeStampResponse(tse.getTimestamp());
		BigInteger sn = tsr.getTimeStampToken().getTimeStampInfo().getSerialNumber();
		HashSet<BigInteger> certSn = new HashSet<BigInteger>();
		certSn.add(sn);
		return  crlService.handleCrls(certSn);
	}


	/*
	 * vrati hash ktory charakterizuje vstupne byty pole vstupnych byto je nezoradene
	 * on to zoradi a prepocita
	 */
	private static byte[] computeNodeHash(List<byte[]> unorderedByteList) throws Exception{
		TreeSet<String> orderedData = new TreeSet<String>();
		MessageDigest hash = MessageDigest.getInstance("SHA", "BC");		
		
		for (byte[] data : unorderedByteList) {
			hash.reset();
			hash.update(data);
			orderedData.add(new String(hash.digest()));
		}
		hash.reset();
		for (String hashData : orderedData) {
			hash.update(hashData.getBytes());
		}
		byte [] result = hash.digest();
		return result;
	}
		
	
	private byte [] comuteHashRoot(ArchivePackageEntity archiv, EvidenceRecordEntity evidence) throws Exception{
		List<EvidenceRecordEntity> list = new ArrayList<EvidenceRecordEntity>(archiv.getArchiveRecord());
		Collections.sort(list);
		
		//najstarsi zaznam
		if(list.get(0).equals(evidence) ){
			 List<byte[]> referenceData = new ArrayList<byte[]>();
			 //pridam obsah podpisu a obsah dat
			// referenceData.add(archiv.getUserData().getSignature().getBytes());
			 for(int i= 0; i< archiv.getUserData().getData().size(); i++){
				 referenceData.add(archiv.getUserData().getData().get(i).getData());
			 }
			 byte[] hashReference = computeNodeHash(referenceData); 
			 
			 //pridam aktualny certifikat
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (CertificateEntity certificate : evidence.getCertificates()) {
				 protectionData.add(certificate.getCert());
			 }
			 for (CrlEntity crl: evidence.getCrls()){
				 protectionData.add(crl.getCrl());
			 }			 
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);			 
			 List<byte[]> timestampData = new ArrayList<byte[]>();
				timestampData.add(hashReference);
				timestampData.add(hashProtectionData);
				
			return computeNodeHash(timestampData);
		}else{
			byte[] hashReference = comuteHashRoot(archiv, list.get(list.indexOf(evidence)-1));
			 
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (CertificateEntity certificate : evidence.getCertificates()) {
				 protectionData.add(certificate.getCert());
			 }
			 for (CrlEntity crl: evidence.getCrls()){
				 protectionData.add(crl.getCrl());
			 }		

			 protectionData.add(list.get(list.indexOf(evidence)-1).getTimestamp().getTimestamp());
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);
			
			 List<byte[]> timestampData = new ArrayList<byte[]>();
			 timestampData.add(hashReference);
			 timestampData.add(hashProtectionData);	
			 return computeNodeHash(timestampData);
		}
	}
	
	private TimestampEntity makeProtection(ArchivePackageEntity archiv,EvidenceRecordEntity entity)
			throws Exception {	
		EvidenceRecordEntity actual = null;
		if(entity == null){
			actual = archiv.getArchiveRecord().iterator().next();
		}else{
			actual = entity;
		}
		byte[] hashData = comuteHashRoot(archiv,actual);
		TimeStampResponse token = communicatorService.getTimestamp(hashData);
		TimestampEntity tse = new TimestampEntity();
		BigInteger sn = token.getTimeStampToken().getTimeStampInfo()
				.getSerialNumber();
		CertificateEntity certificate = certificateService
				.handleCertificate(sn);
		tse.convertTimeStampTokenToEntity(token, archiv, certificate);
		return tse;
	}

	private Set<EvidenceRecordEntity> processArchiveRecord(
			SignatureRequest signatureRequest) throws Exception {
		Set<EvidenceRecordEntity> result = new HashSet<EvidenceRecordEntity>();
		EvidenceRecordEntity ere = new EvidenceRecordEntity();
		ere.setCreated(new Date());

		Set<CertificateEntity> certificates = new HashSet<CertificateEntity>();
		Set<CrlEntity> crls = new HashSet<CrlEntity>();

		JAXBElement<X509DataType> jbDataType = (JAXBElement<X509DataType>) signatureRequest
				.getXades().get(0).getSignature().getKeyInfo().getContent()
				.get(0);
		X509DataType dataType = jbDataType.getValue();
		JAXBElement<CertificateValuesType> jbCertValType = (JAXBElement<CertificateValuesType>) dataType
				.getX509IssuerSerialOrX509SKIOrX509SubjectName().get(0);
		EncapsulatedPKIDataType encPKIdt = (EncapsulatedPKIDataType) jbCertValType
				.getValue().getEncapsulatedX509CertificateOrOtherCertificate()
				.get(0);
		byte[] certByte = encPKIdt.getValue();
		CertificateFactory certFactory = CertificateFactory
				.getInstance("X.509");
		InputStream in = new ByteArrayInputStream(certByte);
		X509Certificate certificate = (X509Certificate) certFactory
				.generateCertificate(in);
		certificates.add(certificateService.handleCertificate(certificate));
		certificates.addAll(certificateService
				.handleCertificate(getCertificates(signatureRequest)));

		crls.add(crlService.handleCrls(getCertificates(signatureRequest)));

		ere.setCertificates(certificates);
		ere.setCrls(crls);
		result.add(ere);
		return result;
	}

	public HashSet<BigInteger> getCertificates(SignatureRequest signatureRequest)
			throws Exception {
		ObjectType ot = signatureRequest.getXades().get(0).getSignature()
				.getObject().get(0);
		JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot
				.getContent().get(0);
		QualifyingPropertiesType qpt = jaxbelement.getValue();

		EncapsulatedPKIDataType edtallDataTS = (EncapsulatedPKIDataType) qpt
				.getSignedProperties().getSignedDataObjectProperties()
				.getAllDataObjectsTimeStamp().get(0)
				.getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse allDataTimestamp = new TimeStampResponse(
				edtallDataTS.getValue());

		EncapsulatedPKIDataType edtinitSigDataTS;
		TimeStampResponse initialSignatureTimestamp;
		if (qpt.getSignedProperties().getSignedDataObjectProperties()
				.getAllDataObjectsTimeStamp().size() > 1) {
			int index = qpt.getSignedProperties()
					.getSignedDataObjectProperties()
					.getAllDataObjectsTimeStamp().size() - 1;
			edtinitSigDataTS = (EncapsulatedPKIDataType) qpt
					.getSignedProperties().getSignedDataObjectProperties()
					.getAllDataObjectsTimeStamp().get(index)
					.getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
			initialSignatureTimestamp = new TimeStampResponse(
					edtinitSigDataTS.getValue());
		} else {
			initialSignatureTimestamp = allDataTimestamp;
		}

		JAXBElement<XAdESTimeStampType> xdssigDataTS = (JAXBElement<XAdESTimeStampType>) qpt
				.getUnsignedProperties()
				.getUnsignedSignatureProperties()
				.getCounterSignatureOrSignatureTimeStampOrCompleteCertificateRefs()
				.get(0);
		EncapsulatedPKIDataType edtSigDataTS = (EncapsulatedPKIDataType) xdssigDataTS
				.getValue().getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse signatureTimestamp = new TimeStampResponse(
				edtSigDataTS.getValue());

		HashSet certId = new HashSet<String>();
		certId.add(allDataTimestamp.getTimeStampToken().getTimeStampInfo()
				.getSerialNumber());
		certId.add(initialSignatureTimestamp.getTimeStampToken()
				.getTimeStampInfo().getSerialNumber());
		certId.add(signatureTimestamp.getTimeStampToken().getTimeStampInfo()
				.getSerialNumber());

		return certId;

	}

	private String generateArchiveDescription(List<String> userFiles) {
		GregorianCalendar gc = new GregorianCalendar();
		StringBuilder sb = new StringBuilder();
		sb.append("Archive created: ").append(gc.get(Calendar.DATE))
				.append("-").append("-").append(gc.get(Calendar.MONTH))
				.append("-").append(gc.get(Calendar.YEAR));
		sb.append(" contain ").append(userFiles.size() - 1)
				.append(" signed data");
		return sb.toString();
	}

	private UserEntity loadUser(UserType user) {
		UserEntity dbuser = userService
				.findUserByUserName(user.getUserName());
		return dbuser;
	}

	private String generateUserToken(UserDataEntity dataEntity)
			throws NoSuchAlgorithmException {
		MessageDigest md = MessageDigest.getInstance("SHA");
		for (int i = 0; i < dataEntity.getData().size(); i++) {
			md.update(dataEntity.getData().get(i).getData());
		}
		String token = new BigInteger(1, md.digest()).toString(16);
		return token;
	}

	private UserDataEntity prepareUserData(SignatureRequest signatureRequest,
			List<String> userFiles) throws IOException {
		UserDataEntity userData = new UserDataEntity();
		StringWriter writer = new StringWriter();
		Result result = new StreamResult(writer);
		marshallerService.marshal(signatureRequest.getXades().get(0), result);
		List<BinaryDataEntity> data = new ArrayList<BinaryDataEntity>(
				userFiles.size());

		for (String file : userFiles) {
			File tmpFile = new File(file);
			FileInputStream fis = new FileInputStream(tmpFile);
			byte[] content = new byte[(int) tmpFile.length()];
			fis.read(content);
			fis.close();
			BinaryDataEntity bde = new BinaryDataEntity();
			bde.setData(content);
			bde.setName(tmpFile.getName());
			data.add(bde);
		}

		userData.setSignature(writer.toString());
		userData.setData(data);
		return userData;
	}
}
